#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@NAME          : utils.py
@TIME          : 2024/11/18 16:38:34
@AUTHOR        : chenlip
@VERSION       : 0.0.1
@DESCRIPTION   : 这是一个公共的工具模块
'''
# --------------------------引用--------------------------
import os, json, html
import imghdr
from datetime import datetime
import urllib.parse
from math import ceil
from PIL import Image
# --------------------------引用--------------------------

class Utils:

    APPLICATION_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
    _now = datetime.now()
    CURYEAR = _now.strftime('%Y') # 当前年份
    CURDAY = _now.strftime('%Y-%m-%d') # 当天
    CURTIME = _now.strftime('%Y-%m-%d %H:%M:%S') # 当前时间
    CURMONTH = _now.strftime('%Y-%m') # 当前月份
    # 系统的临时缓存目录
    TEMPDIR = os.path.join(APPLICATION_ROOT, 'static/temps')

    # 自定义相册图片数据结构
    IMAGE_META = {
        "file_id": "676233b22f4b615ce0cf4922b4086e74c227474f",
        "fname": "IMG_20210101_000001.jpg",
        "file_extension": "jpg",
        "size": 123456,
        "created_at": "2021-01-01T00:00:01Z", # 云端创建时间
        "updated_at": "2021-01-01T00:00:01Z", # 云端更新时间
        "local_created_at": "2021-01-01 08:00:01", # 本地创建时间
        "local_modified_at": "2021-01-01 08:00:01", # 本地更新时间
        "height": 1080,
        "image_media_metadata": {
            "width": 1920,
            "height": 1080,
            "FaceConfidence": 0.0, # 人脸置信度, 1-确认有人脸, 0-确认无人脸
            "Age": 19, # 年龄
            "AgeSD": 4, # 年龄标准差, +-4
            "Gender": "female", # 性别 (需要提供一个默认值或枚举)
            "labels": ["label1", "label2", "label3"], # 图片标签
            "FaceAttributes": { # 人脸属性
                "FaceQuality": 0.897,
                "FaceBoundary": { # 人脸边界及位置
                    "Width": 292,
                    "Height": 413,
                    "Top": 199,
                    "Left": 200
                },
                "Mouth": "open", # 嘴巴状态
                "MouthConfidence": 0.623,
                "Beard": "none", # 胡须
                "BeardConfidence": 0.999,
                "Hat": "none", # 帽子
                "HatConfidence": 1,
                "Mask": "none", # 口罩
                "MaskConfidence": 0.865,
                "Glasses": "none", # 眼镜
                "GlassesConfidence": 1,
                "HeadPose": { # 头部姿态
                    "Pitch": 3.991, # 俯仰角
                    "Roll": 0.181, # 横滚角
                    "Yaw": 18.054 # 偏航角
                }
            },
            "faces_thumbnail": "https://example.com/faces_thumbnail.jpg", # 人脸缩略图
        },
        
        "thumbnail": "https://example.com/thumbnail.jpg", # 缩略图
        "url": "https://example.com/IMG_20210101_000001.jpg", # 大图
    }
    BASE_IMAGE_FILE = {
        "source": "baidupan",
        "file_id": "676233b22f4b615ce0cf4922b4086e74c227474f",
        "cpath": "/", # 云端路径
        "lpath": "/", # 本地路径
        "fname": "IMG_20210101_000001.jpg",
        "ext": "jpg",
        "size": 123456,
        "box":[], # width, height
        "created_at": "2021-02-01 10:03:32", # 云端创建时间
        "updated_at": "2021-02-01 10:03:32", # 云端更新时间
        "local_created_at": "2021-01-01 11:03:32", # 本地创建时间
        "local_modified_at": "2021-01-02 08:00:01", # 本地更新时间
        "watermark": {}, # 水印信息
        "exif": {}, # 图片的exif信息
        "metadata": {}, # 图片的元数据，可以包含图片的理解文本和标签
        "s3_urls": {}, # 图片的S3地址
        "status": "1", # 文件状态
        "note": "no" # 文件备注
    }
    
    def __init__(self):
        pass

    # 显示错误信息
    @staticmethod
    def show_error(msg:str, col:str="red"):
        '''
            显示错误信息
            :Args:
                - msg: 错误信息
                - exit: 是否退出程序
            :Return:
                - None
        '''
        print(Utils.cco(f"[ERROR] {msg}", col))

    # 获取图片文件的宽高数据
    @staticmethod
    def get_image_size(filepath:str) -> list:
        '''
            获取图片文件的宽高数据
            :Args:
                - filepath: 文件路径
            :Return:
                - 文件宽高数据，例如：[1920,1080]
        '''
        if not os.path.exists(filepath):
            Utils.show_error(f"文件不存在,{filepath}")
            return [0,0]
        
        if not os.path.isfile(filepath):
            Utils.show_error(f"这个不是文件,{filepath}")
            return [0,0]
        
        # 判断文件是否为图片文件，后缀名为jpg,jpeg,png,gif,bmp,webp,tiff,tif,ico
        _ext_lst = ["jpg","jpeg","png","gif","bmp","webp","tiff","tif","ico"]
        if os.path.splitext(filepath)[1][1:] not in _ext_lst:
            Utils.show_error(f"不是图片文件,{filepath}, 图像文件后缀名应该是：{_ext_lst}")
            return [0,0]
        
        try:
            img = Image.open(filepath)
            width, height = img.size
            return [width, height]
        except Exception as e:
            Utils.show_error(f"获取图片文件宽高数据失败,{filepath}, {e}")
            return [0,0]

    # 根据文件后缀名称判断文件类型
    @staticmethod
    def getfiletype(filename:str) -> str:
        '''
            根据文件后缀名称判断文件类型
            :Args:
                - filename: 文件名称, 例如：abc.jpg
            :Return:
                - 文件类型, 例如：jpg,truetype,opentype,woff,woff2
        '''
        _fileex = filename.split(".")[-1]
        _filetype = _fileex
        if _fileex == "ttf":
            _filetype = "truetype"
        elif _fileex == "otf":
            _filetype = "opentype"
        elif _fileex == "woff":
            _filetype = "woff"
        elif _fileex == "woff2":
            _filetype = "woff2"

        return _filetype
    
    @staticmethod
    def is_image_file(file, imgext:list=["jpg","jpeg","png","gif","webp","ico","bmp"]) -> bool:
        '''
            判断文件是否为图片文件[支持的图片格式：jpg,jpeg,png,gif,bmp,webp]
            :Args:
                - file: 文件对象，例如：file = request.files['file']
            :Return:
                - True: 是图片文件, False: 不是图片文件
        '''
        file.seek(0)  # 确保文件指针在开头
        file_type = imghdr.what(file) or 'unknown'
        file.seek(0)  # 再次重置文件指针

        _is_img = True if file_type in imgext else False
        return _is_img
    
    @staticmethod
    def check_file_size(file, max_size:int=5) -> bool:
        '''
            检查文件大小是否超过指定大小
            :Args:
                - file: 文件对象，例如：file = request.files['file']
                - max_size: 最大文件大小,单位：MB，默认为5MB
            :Return:
                - True: 文件大小符合要求, False: 文件大小超过要求
        '''
        file.seek(0, os.SEEK_END)
        file_size = file.tell()
        file.seek(0)
        _max_size = max_size * 1024 * 1024
        return file_size <= _max_size
    
    # 根据日期创建一个目录结构
    @staticmethod
    def create_date_dir(root:str, datestr:str) -> str:
        '''
            根据指定根目录和日期创建一个本地的目录结构
            程序逻辑：
                1、判断根目录是否存在年目录，如果不存在则创建年目录
                2、如果存在年目录，则判断是否存在日期目录，如果不存在则创建日期目录
            :Args:
                - root: 根目录路径，例如：/data/pic
                - datestr: 日期字符串，格式为'YYYY-MM-DD'
            :Return:
                - 创建的日期目录路径
        '''
        year_dir = os.path.join(root, datestr.split('-')[0])
        y_path = os.path.join(root, year_dir)
        _d_dir = os.path.join(y_path, datestr)

        if not os.path.exists(y_path):
            # 新创建年目录和日期目录
            os.makedirs(y_path)
            os.makedirs(_d_dir)
        else:
            # 年目录已经存在；判断日期目录是否存在
            if not os.path.exists(_d_dir):
                os.makedirs(_d_dir)

        return _d_dir
    
    # 文件名转码URL
    @staticmethod
    def urlquote(filename:str) -> str:
        '''
            文件名转码URL
            :Args:
                - filename: 文件名称, 例如：微软黑体.ttf
            :Return:
                - 转码后的URL, 例如：%E5%BE%AE%E8%BD%AF%E9%BB%91%E4%BD%93.ttf
        '''
        return urllib.parse.quote(filename)
    
    @staticmethod
    def urlfn_unquote(filename:str) -> str:
        '''
            URL文件名解码
            :Args:
                - filename: 文件名称, 例如：%E5%BE%AE%E8%BD%AF%E9%BB%91%E4%BD%93.ttf
            :Return:
                - 解码后的文件名, 例如：微软黑体.ttf
        '''
        return urllib.parse.unquote(filename)
    
    # func
    
    # 将字符串转换为控制台红色字体
    @staticmethod
    def cco(text:str, colorstr:str="red") -> str:
        '''
            将字符串转换为控制台带颜色的字体
            :Args:
                - text: 要转换的字符串
                - colorstr: 字体颜色, red, green, yellow, blue, purple, cyan
            :Returns:
                - 转换后的字符串
        '''
        _strcol = text

        if (colorstr == "red"): 
            _strcol = '\033[1;31m' + text + '\033[0m'
        elif (colorstr == "green"): 
            _strcol = '\033[1;32m' + text + '\033[0m'
        elif (colorstr == "yellow"): 
            _strcol = '\033[1;33m' + text + '\033[0m'
        elif (colorstr == "blue"): 
            _strcol = '\033[1;34m' + text + '\033[0m'
        elif (colorstr == "purple"): 
            _strcol = '\033[1;35m' + text + '\033[0m'
        elif (colorstr == "cyan"): 
            _strcol = '\033[1;36m' + text + '\033[0m'
        
        return _strcol
    
    # 将ISO 8601格式的时间字符串转换为本地时间（'%Y-%m-%d %H:%M:%S'）字符串
    @staticmethod
    def iso8601_localtime(iso8601_time: str, formatstr:str='%Y-%m-%d %H:%M:%S') -> str:
        dt = datetime.fromisoformat(iso8601_time.replace('Z', '+00:00'))

        # 转换为本地时间
        local_time = dt.astimezone().strftime(formatstr)
        return local_time
    
    # 将Unix时间戳转换为本地时间（'%Y-%m-%d %H:%M:%S'）字符串
    @staticmethod
    def unixtime_localtime(unix_time: int, formatstr:str='%Y-%m-%d %H:%M:%S') -> str:
        dt = datetime.fromtimestamp(unix_time)
        local_time = dt.strftime(formatstr)
        return local_time
    
    @staticmethod
    def _parse_time_string(time_str: str) -> datetime:
        """解析时间字符串为datetime对象"""
        try:
            return datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
        except ValueError as e:
            raise ValueError(f"时间字符串格式错误，应为'YYYY-MM-DD HH:MM:SS'，实际为: {time_str}")
    
    @staticmethod
    def r_w_lst_txt(act:str='r', txtfnpath:str='test.txt', txtlst:list=None) :
        '''
            读写列表(支持列表里面元素使用dict或者list结构)到文本文件, 
            默认是读取文件内容为txt，如果act='w'则写入列表（参数txtlst不能为空）到txt文件（txtfnpath）, 如果act='r'则读取文件内容为列表
            :Args:
                - act: 操作类型, r: 读取, w: 写入
                - txtfnpath: 文本文件路径, 使用完整路径
                - txtlst: 写入的列表
            :Return:
                - 如果act='r'则返回读取的列表
                - 如果act='w'则返回txt文件路径,如果传入列表参数是None, 则返回None
        '''
        _tmp_lst = []
        if act == 'r':
            if os.path.exists(txtfnpath):
                with open(txtfnpath, 'r', encoding='utf-8') as f:
                    for _l in f.readlines():
                        try:
                            # 直接尝试解析JSON，成功则为字典
                            dict_data = json.loads(_l.strip())
                            _tmp_lst.append(dict_data)
                        except json.JSONDecodeError:
                            # JSON解析失败，按普通文本处理
                            _tmp_lst.append(_l.strip().split(","))
                
                return _tmp_lst
            else:
                return None
        elif act == 'w':
            if txtlst:
                with open(txtfnpath, 'w', encoding='utf-8') as f:
                    for _l in txtlst:
                        # 判断 _l 是否为dict
                        if isinstance(_l, dict):
                            f.write(json.dumps(_l)+"\n")
                        else:
                            f.write(','.join([str(item) for item in _l])+"\n")
                
                return txtfnpath
            else:
                return None

    # 将树状结构的字典转换为字符串
    @staticmethod
    def dict2ul(_dict:dict, _level:int=0, parent_path: str='') -> str:
        '''
            将树状结构的字典转换为树状的字符串
            :Args:
                - _dict: 字典, 例如：
                {
                    "目录1": {},
                    "目录2": {
                        "目录2.1": {},
                        "目录2.2": {},
                        "目录2.3": {
                            "目录2.3.1": {},
                            "目录2.3.2": {},
                        }
                    },
                    "目录3": {},
                }

                - _level: 层级
            
            :Return:
                - html字符串
                    <ul class="tree">
                        <li data-path="目录1">目录1
                            <ul>
                                <li data-path="目录1\北京研发部">北京研发部</li>
                                <li data-path="目录1\深圳研发部">深圳研发部
                                    <ul>
                                        <li data-path="目录1\深圳研发部\产品1组">产品1组</li>
                                        <li data-path="目录1\深圳研发部\产品2组">产品2组</li>
                                    </ul>
                                </li>
                            </ul>
                        </li>
                        <li data-path="目录2">目录2
                            <ul>
                                <li data-path="目录2\售前">售前</li>
                                <li data-path="目录2\售后">售后</li>
                                <li data-path="目录2\代理分区">代理分区
                                    <ul>
                                        <li data-path="目录2\代理分区\东北区">东北区</li>
                                        <li data-path="目录2\代理分区\华北区">华北区</li>
                                        <li data-path="目录2\代理分区\华南区">华南区</li>
                                        <li data-path="目录2\代理分区\华中区">华中区</li>
                                    </ul>
                                </li>
                            </ul>
                        </li>
                        <li data-path="目录2">目录3</li>
                        <li data-path="目录2">目录4</li>
                    </ul>
        '''
        html = ''
        indent = '    ' * _level # 缩进
        if (_level == 0):
            html += f'{indent}<ul class="tree">\n'
        else:
            html += f'{indent}<ul>\n'
        
        for key, value in _dict.items():
            current_path = f'{parent_path}/{key}' if parent_path else key
            if isinstance(value, dict):
                if value:
                    html += f'{indent}    <li data-path="{current_path}">{key}\n'
                    html += Utils.dict2ul(value, _level + 1, current_path)
                    html += f'{indent}    </li>\n'
                else:
                    html += f'{indent}    <li data-path="{current_path}">{key}</li>\n'
            else:
                html += f'{indent}    <li data-path="{current_path}">{key}</li>\n'
        
        html += f'{indent}</ul>\n'

        return html
    
    @staticmethod
    def list_pagination(_list:list, _page:int=1, _pagesize:int=10, imgurlpath:str=None) -> list:
        '''
            对列表进行分页, 返回分页后的列表/总页数/总数量/bootstrap分页html字符穿
            例如：[分页后的list, totalpage, totalcount, pagehtml]
            pagehtml = '
                <li class="page-item disabled"><a class="page-link">上一页</a></li>
                <li class="page-item active" aria-current="page"><span class="page-link">1</span></li>
                <li class="page-item"><a class="page-link" href="#">2</a></li>
                <li class="page-item"><a class="page-link" href="#">3</a></li>
                <li class="page-item"><a class="page-link" href="#">下一页</a></li>
            '
            pagehtml 相关的逻辑：
            1. 当前页码为1时，上一页按钮为disabled
            2. 当前页码为最后一页时，下一页按钮为disabled
            3. 当前页码为n时，第n页为active，其他页码为可点击，点击后跳转到对应页码
            4. 当前页码为n时，显示当前页码前后的页码，最多显示前后2个页码，超过2个页码时，显示省略号
            :Args:
                - _list: 列表
                - _page: 当前页码
                - _pagesize: 每页显示的数量
                - imgurlpath: minio url路径
            :Return:
                - [分页后的list, totalpage, totalcount, pagehtml]
        '''
        _start = (_page - 1) * _pagesize
        _end = _page * _pagesize
        totalcount = len(_list)

        if totalcount == 0:
            return [[], 0, 0, '']
        
        totalpage = max(1, ceil(totalcount / _pagesize))

        paginated_list = _list[_start:_end]
        
        imgurlpath = "" if imgurlpath is None else imgurlpath # minio url路径, 例如：images/2021/11/18
        imgurlpath = html.escape(imgurlpath)  
        # 构建分页导航 HTML
        pagehtml = ''

        # 上一页按钮
        if _page > 1:
            url_js = "javascript:getImgList('"+imgurlpath+"',"+ str((_page - 1)) +")"
            pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">上一页</a></li>'
        else:
            pagehtml += '<li class="page-item disabled"><a class="page-link">上一页</a></li>'

        # 页码按钮
        if totalpage <= 5:
            for i in range(1, totalpage + 1):
                if i == _page:
                    pagehtml += f'<li class="page-item active" aria-current="page"><span class="page-link">{i}</span></li>'
                else:
                    #url_js = html.escape(f'javascript:getImgList("{imgurlpath}",{i})')
                    url_js = "javascript:getImgList('"+imgurlpath+"',"+ str(i) +")"
                    pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">{i}</a></li>'
        else:
            if _page <= 3:
                for i in range(1, 4):
                    if i == _page:
                        pagehtml += f'<li class="page-item active" aria-current="page"><span class="page-link">{i}</span></li>'
                    else:
                        url_js = "javascript:getImgList('"+imgurlpath+"',"+ str(i) +")"
                        pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">{i}</a></li>'
                pagehtml += '<li class="page-item disabled"><a class="page-link">...</a></li>'
                url_js = "javascript:getImgList('"+imgurlpath+"',"+ str(totalpage) +")"
                pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">{totalpage}</a></li>'
            elif _page > totalpage - 3:
                url_js = "javascript:getImgList('"+imgurlpath+"',1)"
                pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">1</a></li>'
                pagehtml += '<li class="page-item disabled"><a class="page-link">...</a></li>'
                for i in range(totalpage - 2, totalpage + 1):
                    if i == _page:
                        pagehtml += f'<li class="page-item active" aria-current="page"><span class="page-link">{i}</span></li>'
                    else:
                        url_js = "javascript:getImgList('"+imgurlpath+"',"+ str(i) +")"
                        pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">{i}</a></li>'
            else:
                url_js = "javascript:getImgList('"+imgurlpath+"',1)"
                pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">1</a></li>'
                pagehtml += '<li class="page-item disabled"><a class="page-link">...</a></li>'
                for i in range(_page - 1, _page + 2):
                    if i == _page:
                        pagehtml += f'<li class="page-item active" aria-current="page"><span class="page-link">{i}</span></li>'
                    else:
                        url_js = "javascript:getImgList('"+imgurlpath+"',"+ str(i) +")"
                        pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">{i}</a></li>'
                pagehtml += '<li class="page-item disabled"><a class="page-link">...</a></li>'
                url_js = "javascript:getImgList('"+imgurlpath+"',"+ str(totalpage) +")"
                pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">{totalpage}</a></li>'

        # 下一页按钮
        if _page < totalpage:
            url_js = "javascript:getImgList('"+imgurlpath+"',"+ str((_page + 1)) +")"
            pagehtml += f'<li class="page-item"><a class="page-link" href="{url_js}">下一页</a></li>'
        else:
            pagehtml += '<li class="page-item disabled"><a class="page-link">下一页</a></li>'


        return [paginated_list, totalpage, totalcount, pagehtml]

